#include "hocodeedit.h"
#include <QDebug>

HoLineNumberWidget::HoLineNumberWidget(HoCodeEdit *edit, QScrollArea *area)
    : QWidget(area)
{
    this->codeEditor = edit;
}

int HoLineNumberWidget::lineNumberAreaWidth() const {
    int digits = 1;
    int maxLines = qMax(1, this->codeEditor->blockCount());
    while (maxLines >= 10) {
        maxLines /= 10;
        ++digits;
    }
    int space = 3 + this->codeEditor->fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
    return space;
}

void HoLineNumberWidget::paintEvent(QPaintEvent *e)  {
    this->codeEditor->lineNumberAreaPaintEvent(e);
}

HoCodeEdit::HoCodeEdit(QStringList keywordPatterns, QWidget *parent) : QTextEdit(parent)
{
    QStringList _keywordPatterns;
    for (const QString &str : keywordPatterns) {
        _keywordPatterns << QString("\\b") + str + QString("\\b");
    }
    this->highlighter = new HoSyntaxHighlighter(_keywordPatterns, this->document());
    this->completer = new HoAutoCompleter(keywordPatterns, this);
    this->completer->setWidget(this);
    this->completer->setCompletionMode(QCompleter::PopupCompletion);
    this->completer->setCaseSensitivity(Qt::CaseInsensitive);

    connect(this->completer, QOverload<const QString &>::of(&QCompleter::activated), this, &HoCodeEdit::insertCompletion);

    this->highlightCurrentLine();
    this->installEventFilter(this);
    this->setLineWrapMode(QTextEdit::NoWrap);

    this->scrollArea = nullptr;
    this->lineNumberArea = nullptr;
}

void HoCodeEdit::showLineNumber() {
    scrollArea = new QScrollArea(this);
    lineNumberArea = new HoLineNumberWidget(this, scrollArea);
    scrollArea->setWidget(lineNumberArea);
    scrollArea->setWidgetResizable(true);
    scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    scrollArea->setFrameShape(QFrame::NoFrame);
    lineNumberArea->show();
    scrollArea->show();
    connect(this->document(), &QTextDocument::blockCountChanged, this, &HoCodeEdit::updateLineNumberAreaWidth);
    connect(this->verticalScrollBar(), &QScrollBar::valueChanged, this, &HoCodeEdit::syncVScroll);
    connect(this->horizontalScrollBar(), &QScrollBar::valueChanged, this, &HoCodeEdit::syncHScroll);
    connect(this, &QTextEdit::cursorPositionChanged, this, &HoCodeEdit::highlightCurrentLine);
    connect(this, &QTextEdit::textChanged, this, &HoCodeEdit::updateLineNumberAreaHeight);
    this->updateLineNumberAreaWidth(0);
}

void HoCodeEdit::syncVScroll(int value) {
    if (this->scrollArea != nullptr) {
        scrollArea->verticalScrollBar()->setValue(value);
    }
}

void HoCodeEdit::syncHScroll(int value) {
    if (this->scrollArea != nullptr) {
        scrollArea->horizontalScrollBar()->setValue(value);
    }
}

int HoCodeEdit::lineNumberAreaWidth() {
    int digits = 1;
    int max = qMax(1, blockCount());
    while (max >= 10) {
        max /= 10;
        ++digits;
    }

    int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;

    return space;
}

void HoCodeEdit::resizeEvent(QResizeEvent *event) {
    QTextEdit::resizeEvent(event);
    if (this->scrollArea != nullptr) {
        QRect cr = contentsRect();
        scrollArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberArea->lineNumberAreaWidth(), cr.height()));
    }
}

void HoCodeEdit::updateLineNumberAreaWidth(int /* newBlockCount */) {
    setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}

void HoCodeEdit::highlightCurrentLine() {
    QList<QTextEdit::ExtraSelection> extraSelections;

    if (!isReadOnly()) {
        QTextEdit::ExtraSelection selection;

        QColor lineColor = QColor(Qt::yellow).lighter(160);

        selection.format.setBackground(lineColor);
        selection.format.setProperty(QTextFormat::FullWidthSelection, true);
        selection.cursor = textCursor();
        selection.cursor.clearSelection();
        extraSelections.append(selection);
    }

    setExtraSelections(extraSelections);
}

QRectF HoCodeEdit::blockBoundingGeometry(const QTextBlock &block) const {
    return document()->documentLayout()->blockBoundingRect(block);
}

QPointF HoCodeEdit::contentOffset() const {
    return QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value());
}

QRectF HoCodeEdit::blockBoundingRect(const QTextBlock &block) const {
    return document()->documentLayout()->blockBoundingRect(block);
}

int HoCodeEdit::blockCount() const {
    return document()->blockCount();
}

QTextBlock HoCodeEdit::firstVisibleBlock() const {
    QTextCursor cursor = textCursor();
    cursor.movePosition(QTextCursor::Start);
    while (cursor.block().isValid()) {
        QRectF blockRect = blockBoundingGeometry(cursor.block()).translated(contentOffset());
        if (blockRect.bottom() >= 0) {
            return cursor.block();
        }
        cursor.movePosition(QTextCursor::NextBlock);
    }
    return cursor.block();
}

QString HoCodeEdit::getText() const {
    return this->toPlainText();
}

void HoCodeEdit::keyPressEvent(QKeyEvent *e) {
    if (this->lineNumberArea != nullptr) {
        this->lineNumberArea->update();
    }

    if (completer && completer->popup()->isVisible()) {
        switch (e->key()) {
        case Qt::Key_Enter:
        case Qt::Key_Return:
        case Qt::Key_Escape:
        case Qt::Key_Tab:
        case Qt::Key_Backtab:
            e->ignore();
            return;
        default:
            break;
        }
    }

    QTextEdit::keyPressEvent(e);

    const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
    if (!completer || (ctrlOrShift && e->text().isEmpty())) {
        return;
    }

    if (e->key() == Qt::Key_Backspace) {
        completer->popup()->hide();
        return;
    }

    static QString eow("~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-=");
    bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift;
    QString completionPrefix = textUnderCursor();

    if (!isShortcut(e) && (hasModifier || e->text().isEmpty() || completionPrefix.length() < 1 || eow.contains(e->text().right(1)))) {
        completer->popup()->hide();
        return;
    }

    if (completionPrefix != completer->completionPrefix()) {
        completer->setCompletionPrefix(completionPrefix);
        completer->popup()->setCurrentIndex(completer->completionModel()->index(0, 0));
    }

    QRect cr = cursorRect();
    cr.setWidth(completer->popup()->sizeHintForColumn(0) + completer->popup()->verticalScrollBar()->sizeHint().width());
    completer->complete(cr);
}

QString HoCodeEdit::textUnderCursor() const {
    QTextCursor tc = this->textCursor();
    tc.select(QTextCursor::WordUnderCursor);
    return tc.selectedText();
}

bool HoCodeEdit::isShortcut(QKeyEvent *e) const {
    return e->modifiers() & Qt::ControlModifier && e->key() == Qt::Key_Space;
}

void HoCodeEdit::insertCompletion(const QString &completion) {
    if (this->completer->widget() != this) {
        return;
    }
    QTextCursor tc = textCursor();
    int extra = completion.length() - this->completer->completionPrefix().length();
    tc.movePosition(QTextCursor::Left);
    tc.movePosition(QTextCursor::EndOfWord);
    tc.insertText(completion.right(extra));
    this->setTextCursor(tc);
}

void HoCodeEdit::lineNumberAreaPaintEvent(QPaintEvent *event) {
    if (this->lineNumberArea != nullptr && this->scrollArea != nullptr) {
        QPainter painter(lineNumberArea);
        painter.fillRect(event->rect(), Qt::lightGray);

        QTextBlock block = firstVisibleBlock();
        int blockNumber = block.blockNumber();
        int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
        int bottom = top + static_cast<int>(blockBoundingRect(block).height());
        double ext = 1.05;
        int offset = 5;

        while (block.isValid()) {
            QString number = QString::number(blockNumber + 1);
            painter.setPen(Qt::black);
            painter.drawText(0, blockNumber * fontMetrics().height() * ext + offset,
                             lineNumberArea->width(), fontMetrics().height() + offset,
                             Qt::AlignRight, number);

            block = block.next();
            top = bottom;
            bottom = top + static_cast<int>(blockBoundingRect(block).height());
            ++blockNumber;
        }
        this->lineNumberArea->setFixedHeight(blockNumber * fontMetrics().height() * ext + offset);

        QScrollBar *this_vbar = this->verticalScrollBar();
        QScrollBar *this_hbar = this->horizontalScrollBar();

        QScrollBar *area_vbar = this->scrollArea->verticalScrollBar();
        QScrollBar *area_hbar = this->scrollArea->horizontalScrollBar();
        area_vbar->setValue(this_vbar->value());
        area_hbar->setValue(this_hbar->value());
    }
}

void HoCodeEdit::updateLineNumberAreaHeight() {
    if (this->lineNumberArea != nullptr) {
        int height = contentsRect().height();
        lineNumberArea->setFixedHeight(height);

        QScrollBar *this_vbar = this->verticalScrollBar();
        QScrollBar *this_hbar = this->horizontalScrollBar();

        QScrollBar *area_vbar = this->scrollArea->verticalScrollBar();
        QScrollBar *area_hbar = this->scrollArea->horizontalScrollBar();
        area_vbar->setValue(this_vbar->value());
        area_hbar->setValue(this_hbar->value());
    }
}
